package org.bioversityinternational.mgbms.ws.impl;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;
import javax.mail.MessagingException;
import javax.servlet.http.HttpServletRequest;
import javax.xml.ws.handler.MessageContext;

import org.apache.cxf.interceptor.security.AccessDeniedException;
import org.apache.cxf.interceptor.security.AuthenticationException;
import org.apache.cxf.jaxws.context.WebServiceContextImpl;
import org.apache.cxf.transport.http.AbstractHTTPDestination;
import org.apache.log4j.Logger;
import org.apache.log4j.Priority;
import org.bioversityinternational.mgbms.model.dao.AccessionDao;
import org.bioversityinternational.mgbms.model.dao.AddressDao;
import org.bioversityinternational.mgbms.model.dao.CentreDao;
import org.bioversityinternational.mgbms.model.dao.CentreTypeDao;
import org.bioversityinternational.mgbms.model.dao.ContactDao;
import org.bioversityinternational.mgbms.model.dao.CountryDao;
import org.bioversityinternational.mgbms.model.dao.LoginDao;
import org.bioversityinternational.mgbms.model.dao.MaterialTypeDao;
import org.bioversityinternational.mgbms.model.dao.NewCentreDao;
import org.bioversityinternational.mgbms.model.dao.OrderDao;
import org.bioversityinternational.mgbms.model.dao.OrderStatusDao;
import org.bioversityinternational.mgbms.model.dao.ShipmentDao;
import org.bioversityinternational.mgbms.model.dao.base.BaseDao;
import org.bioversityinternational.mgbms.model.domain.Accession;
import org.bioversityinternational.mgbms.model.domain.Address;
import org.bioversityinternational.mgbms.model.domain.AssOrderAccession;
import org.bioversityinternational.mgbms.model.domain.AssOrderAccessionId;
import org.bioversityinternational.mgbms.model.domain.AssShipmentAccession;
import org.bioversityinternational.mgbms.model.domain.Centre;
import org.bioversityinternational.mgbms.model.domain.Contact;
import org.bioversityinternational.mgbms.model.domain.ContactCentre;
import org.bioversityinternational.mgbms.model.domain.ContactCentreId;
import org.bioversityinternational.mgbms.model.domain.Login;
import org.bioversityinternational.mgbms.model.domain.MaterialType;
import org.bioversityinternational.mgbms.model.domain.NewCentre;
import org.bioversityinternational.mgbms.model.domain.Order;
import org.bioversityinternational.mgbms.model.domain.Shipment;
import org.bioversityinternational.mgbms.notify.EmailNotifier;
import org.bioversityinternational.mgbms.pdf.SmtaGenerator;
import org.bioversityinternational.mgbms.text.Helper;
import org.bioversityinternational.mgbms.text.TextConstants;
import org.bioversityinternational.mgbms.ws.MgbmsService;
import org.hibernate.HibernateException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@SOAPBinding(style = SOAPBinding.Style.DOCUMENT)
@WebService(endpointInterface = "org.bioversityinternational.mgbms.ws.MgbmsService")
public class MgbmsServiceImpl implements MgbmsService
{
	static private final Logger LOG = Logger.getLogger(MgbmsServiceImpl.class);
	
	@Autowired private AccessionDao accessionDao;
	@Autowired private AddressDao addressDao;
	@Autowired private CentreDao centreDao;
	@Autowired private CentreTypeDao centreTypeDao;
	@Autowired private CountryDao countryDao;
	@Autowired private ContactDao contactDao;
	@Autowired private LoginDao loginDao;
	@Autowired private MaterialTypeDao materialTypeDao;
	@Autowired private NewCentreDao newCentreDao;
	@Autowired private OrderDao orderDao;
	@Autowired private OrderStatusDao orderStatusDao;
	@Autowired private ShipmentDao shipmentDao;
	
	@Autowired private EmailNotifier emailNotifier;
	
	@Override
	@Transactional(propagation=Propagation.REQUIRED, readOnly=true)
	public HashMap<Integer, String> getListOfAvailableCentres()
	{
		return centreDao.listValidCentres();
	}
	
	@Override
	@SuppressWarnings({ "unchecked", "rawtypes" })
	@Transactional(propagation=Propagation.REQUIRED, readOnly=true)
	public HashMap<String, DTO> getObjectFieldsByIDs(String objectClassSimpleName, List<String> idList, List<String> fieldNames) throws AccessDeniedException
	{
		try
		{
			BaseDao associatedClassDao = getDaoByDomainClass(Class.forName(Centre.class.getPackage().getName() + "." + objectClassSimpleName));
			if (associatedClassDao == null)
				throw new IllegalArgumentException("Unable to find DAO for domain class " + objectClassSimpleName);
	
			HashMap<String, DTO> result = new HashMap<String, DTO>();
			for (String id : idList)
			{		
				Map<String, Serializable> tempResult = associatedClassDao.getFieldsById(id, fieldNames);
				if (tempResult == null)
					result.put(id, null);
				else
				{
					DTO dto = new DTO();
					for (String sKey : tempResult.keySet())
						dto.set(sKey, tempResult.get(sKey));
					
					result.put(id, dto);
				}
			}		
			
			return result;
		}
		catch (Exception e)
		{
			LOG.error("Error getting object fields by ID", e);
			throw new AccessDeniedException(e.getMessage());
		}
	}
		
	
	@Override
	@Transactional(propagation=Propagation.REQUIRED, readOnly=true)
    public DTO getContactByEmail(String email) throws AccessDeniedException
    {
		try
		{
			return domainToDTO(contactDao.getByEmail(email), Arrays.asList(new String[] {"address", "address.country", "country"}));
		}
		catch (HibernateException he)
		{
			LOG.error("Unable to get contact by email", he);
			throw new AccessDeniedException(he.getMessage());
		}
    }
    
	@Override
	@Transactional(propagation=Propagation.REQUIRED, readOnly=true)
	public DTO getCurrentCentreForContact(Integer contactId)
	{
		ContactCentre cc = centreDao.getLatestCentreForContact(contactId);
		return domainToDTO(cc == null ? null : cc.getCentre(), Arrays.asList(new String[] {"address", "address.country", "centreType", "centre"}));
	}
	
	@Override
	@Transactional(propagation=Propagation.REQUIRED, readOnly=true)
	public HashMap<Integer, String> getListOfAvailableCountries()
	{
		return countryDao.listValidCountries();
	}

	@Override
	@Transactional(propagation=Propagation.REQUIRED, readOnly=true)
	public HashMap<Integer, String> getListOfAvailableCentreTypes()
	{
		return centreTypeDao.listValidCentreTypes();
	}
	
	@Override
	@Transactional(propagation=Propagation.REQUIRED, readOnly=true)
	public DTO getAddressById(Integer id)
	{
		return domainToDTO(addressDao.read(id), Arrays.asList(new String[] {"country"}));
	}
	
	@Override
	@Transactional(propagation=Propagation.REQUIRED, readOnly=true)
	public DTO getCentreById(Integer id)
	{
		return domainToDTO(centreDao.read(id), Arrays.asList(new String[] {"address", "address.country"}));
	}
	
	@Override
	@Transactional(propagation=Propagation.REQUIRED)
    public Integer saveOrUpdateUserAccount(DTO contactDetails, DTO addressDetails, Integer centreId, DTO newCentreDetails, DTO newCentreAddressDetails) throws AccessDeniedException
    {		
		if (contactDetails == null || addressDetails == null)
			throw new AccessDeniedException("contactDetails and addressDetails must be provided and fully filled-in");

		if (centreId == null && (newCentreDetails == null || newCentreAddressDetails == null))
			throw new AccessDeniedException("If centreId is null then newCentreDetails and newCentreAddressDetails must be fully filled-in");
		
		String contactEmail = contactDetails.getString("email");
		Integer contactId = contactDetails.getInt("id");		
		Contact ctc = contactId == null ? new Contact() : contactDao.read(contactId);	// if no id was passed in then it's a new user and we must create a contact account

		if (contactId == null /* case 1 */ || (contactEmail != null && !contactEmail.equals(ctc.getEmail())) /* case 2 */)
		{	// case 1: we are going to add a new contact 
			// case 2: we are going to update a contact email
			// in both cases, we should have been provided with a valid email address, which must not be present in our contact table
			if (!Helper.isEmailValid(contactEmail))
				throw new AccessDeniedException("You must provide a valid e-mail address in contactDetails.email");
			
			if (contactDao.countByUniqueAttribute("email", contactEmail) > 0)
				throw new AccessDeniedException("A user with this e-mail address already exists: " + contactEmail);
		}

		Address address = ctc.getAddress();
		if (address == null)
			address = new Address();

		try
		{
			domainFromDTO(address, addressDetails);		// here we either fill-up a new domain object, or update an existing one 
		}
		catch (Exception e)
		{
			LOG.error("Error filling-up Address from DTO", e);
			throw new AccessDeniedException(e.getMessage());
		}		
		addressDao.saveOrUpdate(address);		
		
		try
		{
			domainFromDTO(ctc, contactDetails);			// here we either fill-up a new domain object, or update an existing one
		}
		catch (Exception e)
		{
			LOG.error("Error filling-up Contact from DTO", e);
			throw new AccessDeniedException(e.getMessage());
		}
		ctc.setAddress(address);
		contactDao.saveOrUpdate(ctc);

		
		Centre centreToApply = centreDao.read(centreId);
		if (centreToApply != null)
		{	// this contact will be linked to an existing centre
			Date now = new Date();
			boolean fAlreadyLinkedToCentre = false;
			if (contactId != null)
			{	// contact already exists
				ContactCentre currentContactCentreInDB = centreDao.getLatestCentreForContact(contactId);
				if (currentContactCentreInDB != null)
				{
					fAlreadyLinkedToCentre = currentContactCentreInDB.getCentre().equals(centreToApply);
					if (!fAlreadyLinkedToCentre)
						currentContactCentreInDB.setEndDate(now);	// relationship finishes now
				}
			}
			if (!fAlreadyLinkedToCentre)
			{
				ContactCentre cc = new ContactCentre(new ContactCentreId(centreId, ctc.getId()), ctc, centreToApply);
				cc.setStartDate(now);	// relationship starts now
				contactDao.getSession().save(cc);
			}
		}
		
		boolean fLoginRecordExists = ctc.getLogins().size() == 1;
		Login login = !fLoginRecordExists ? new Login() : ctc.getLogins().iterator().next();

		NewCentre newCentre = null;
		if (newCentreDetails != null && newCentreAddressDetails != null)
		{	// this contact will be linked to a "new" centre, or to an existing centre for which details need to be corrected			
			newCentre = login.getNewCentre();
			if (newCentre == null)
				newCentre = new NewCentre();

			Address newCentreAddress = newCentre.getAddress();
			if (newCentreAddress == null)
				newCentreAddress = new Address();
			
			try
			{
				domainFromDTO(newCentreAddress, newCentreAddressDetails);
			}
			catch (Exception e)
			{
				LOG.error("Error filling-up Address from DTO (for NewCentre)", e);
				throw new AccessDeniedException(e.getMessage());
			}
			addressDao.saveOrUpdate(newCentreAddress);
			
			try
			{
				domainFromDTO(newCentre, newCentreDetails);
			}
			catch (Exception e)
			{
				LOG.error("Error filling-up NewCentre from DTO", e);
				throw new AccessDeniedException(e.getMessage());
			}
			newCentre.setAddress(newCentreAddress);
			newCentreDao.saveOrUpdate(newCentre);
		}

		login.setNewCentre(newCentre);
		if (!fLoginRecordExists)
		{
			login.setContact(ctc);
			loginDao.generateRandomPassword(login, false);
			try
			{
				emailNotifier.notifyNewPassword(login, true);
			}
			catch (Exception e)
			{
				LOG.error("Unable to send notification e-mail", e);
				throw new AccessDeniedException(e.getMessage());
			}
		}
		loginDao.saveOrUpdate(login);	
		
    	return login.getId();
    }
	

	@Override
	@Transactional(propagation=Propagation.REQUIRED)
	public void generateNewUserPassword(String userName, String pwd) throws AuthenticationException, AccessDeniedException
	{
		Login login = loginDao.getByUniqueAttributeCombination(new String[] {"contact.email", "password"}, new String[] {userName, pwd});
		if (login == null)
			throw new AuthenticationException("Wrong username or password");

		loginDao.generateRandomPassword(login, false);
		try
		{
			emailNotifier.notifyNewPassword(login, false);
		}
		catch (Exception e)
		{
			LOG.error("Unable to send notification e-mail", e);
			throw new AccessDeniedException(e.getMessage());
		}
	}

	@Override
	@Transactional(propagation=Propagation.REQUIRED, readOnly=true)
	public DTO getUserDetails(String userName, String pwd) throws AuthenticationException
	{
		Login login = loginDao.getByUniqueAttributeCombination(new String[] {"contact.email", "password"}, new String[] {userName, pwd});
		if (login == null)
			throw new AuthenticationException("Wrong username or password");
		
		return domainToDTO(login, Arrays.asList(new String[] {"contact", "contact.address", "newCentre", "newCentre.address", "newCentre.address.country", "newCentre.centreType", "newCentre.country", "newCentre.partOf"}));
	}
	
	@Override
	public String generateRegistrationCode(String sUserEmail) throws AccessDeniedException
	{
		if (!Helper.isEmailValid(sUserEmail))
			throw new AccessDeniedException(sUserEmail + " is not a valid email-address");
		
		try
		{
			Contact ctc = contactDao.getByEmail(sUserEmail);
			String sRegCode = BaseDao.generateRandomPassword(12);
			emailNotifier.sendRegistrationCode(sUserEmail, sRegCode, ctc != null);
			return sRegCode;
		}
		catch (Exception e)
		{
			LOG.error("Unable to send registration code to e-mail: " + sUserEmail, e);
			throw new AccessDeniedException(e.getMessage());
		}
	}

	@Override
	@Transactional(propagation=Propagation.REQUIRED, readOnly=true)
	public HashMap<Integer, String> getAvailableMaterialTypesForAccession(String itcCode) throws AccessDeniedException
	{
		Accession acc = accessionDao.getByUniqueAttribute("itcCode", itcCode);
		if (acc == null)
			throw new AccessDeniedException("No accession exists with ITC code " + itcCode);
		
		HashMap<Integer, String> result = new HashMap<Integer, String>();
		List<MaterialType> mtList = materialTypeDao.findAll();
		for (MaterialType mt : mtList)
			result.put(mt.getId(), mt.getLabel());
		
		return result;
	}
	
	@Override
	@Transactional(propagation=Propagation.REQUIRED)
	public Integer createOrderRequest(Integer contactId, String sAuthorisedOfficialName, String sAuthorisedOfficialAddress, String acceptanceMethod, String paymentMethod, HashMap<String, Integer> accessionsAlongWithMaterialTypes, String purpose) throws AccessDeniedException
	{
		LOG.debug("createOrderRequest called for contact " + contactId);

		Date requestDate = new Date();

		Contact ctc = contactDao.read(contactId);
		if (ctc == null)
			throw new AccessDeniedException("No contact record exists with id " + contactId);

		ContactCentre latestCentreForContact = centreDao.getLatestCentreForContact(contactId);
		if (latestCentreForContact.getEndDate() != null && latestCentreForContact.getEndDate().before(new Date()))
			throw new AccessDeniedException("Contact " + contactId + " is currently not associated to any centre");
		
		if (!ACCEPTANCE_METHOD_SIGNATURE.equals(acceptanceMethod) && !ACCEPTANCE_METHOD_CLICKWRAP.equals(acceptanceMethod) && !ACCEPTANCE_METHOD_SHRINKWRAP.equals(acceptanceMethod))
			throw new AccessDeniedException("'" + acceptanceMethod + "' is not a valid acceptance method");
		
		if (!PAYMENT_METHOD_6_7.equals(paymentMethod) && !PAYMENT_METHOD_6_11.equals(paymentMethod))
			throw new AccessDeniedException("'" + paymentMethod + "' is not a valid payment method");
		
		String[] itcCodes = accessionsAlongWithMaterialTypes.keySet().toArray(new String[accessionsAlongWithMaterialTypes.size()]);
		List<Accession> wantedAccessions = new ArrayList<Accession>();
		for (String itcCode : itcCodes)
		{
			Accession a = accessionDao.getByUniqueAttribute("itcCode", itcCode);
			if (a == null)
				throw new AccessDeniedException("Accession with ITC-code " + itcCode + " does not exist");
			
			wantedAccessions.add(a);
		}
		
		LOG.debug("number of accessions in order request: " + wantedAccessions.size());

		Order order = new Order();
		order.setContact(ctc);
		order.setRequestdate(requestDate);
		order.setAcceptanceMethod(acceptanceMethod);
		order.setPaymentMethod(paymentMethod);
		order.setCentreType(latestCentreForContact.getCentre().getCentreType());
		order.setAuthorisedOfficialName(sAuthorisedOfficialName);
		order.setAuthorisedOfficialAddress(sAuthorisedOfficialAddress);
		order.setStudypurpose(purpose);		
		
		List<String> purposeList = Arrays.asList(TextConstants.get("order.purposeList").split(";"));
		order.setOrderStatus(orderStatusDao.read(purposeList.contains(purpose) ? OrderStatusDao.ORDER_STATUS_INITIAL : OrderStatusDao.ORDER_STATUS_CANCELLED));
		Integer orderId = orderDao.create(order);		// save order now so we can use its generated ID			
		
		LOG.debug("order created with id: " + orderId);

		Set<AssOrderAccession> accessionAssociations = order.getAssOrderAccessions();
		for (Accession a : wantedAccessions)
		{
			Integer materialType = accessionsAlongWithMaterialTypes.get(a.getItcCode());
			AssOrderAccession aoa = new AssOrderAccession(new AssOrderAccessionId(orderId, a.getId(), materialType), a, order, materialTypeDao.read(materialType));
			accessionAssociations.add(aoa);
			orderDao.getSession().save(aoa);
		}
		order.setAssOrderAccessions(accessionAssociations);
		orderDao.update(order);

		LOG.debug(accessionAssociations.size() + " order-to-accession associations added for order: " + orderId);

		if (purposeList.contains(purpose))	// purpose is one of the predefined ones so we can carry on with SMTA generation and setting the order date in case of clickwrap or shrinkwrap acceptance method
		{
			LOG.info("purpose \"" + purpose + "\" is valid so let's generate SMTA doc for order n." + orderId);

			List<String> smtaDocs = getSMTA(orderId);
			if (smtaDocs.size() == 1 /* no addendum */ && !acceptanceMethod.equals(MgbmsService.ACCEPTANCE_METHOD_SIGNATURE))
			{	// we don't need to wait for any postal mail to consider the SMTA signed
				order.setOrderdate(requestDate);
				orderDao.update(order);
			}
		}
		
		try
		{
			emailNotifier.notifyNewOrderRequest(order);
		}
		catch (Exception e)
		{
			LOG.error("Unable to send notification e-mail", e);
			throw new AccessDeniedException(e.getMessage());
		}
		return orderId;
	}
	
	@Override
	public String getSampleSMTA() throws AccessDeniedException
	{
		try
		{
			return tryAndConvertPathToURL(new File(SmtaGenerator.TEMPLATE_DOC_LOCATION + SmtaGenerator.sMainFile));
		}
		catch (Exception e)
		{
			throw new AccessDeniedException(e.getMessage());
		}
	}
	
	@Override
	public LinkedHashMap<String, String> getSmtaAcceptanceMethods() throws AccessDeniedException
	{
		LinkedHashMap<String, String> acceptanceMethodsWithSampleURLs = new LinkedHashMap<String, String>();
		for (String sAcceptanceMethod : acceptanceMethods)
			try
			{
				 String url = tryAndConvertPathToURL(new File(SmtaGenerator.TEMPLATE_DOC_LOCATION + (ACCEPTANCE_METHOD_CLICKWRAP.equals(sAcceptanceMethod) ? SmtaGenerator.sArticle10option3File : (ACCEPTANCE_METHOD_SIGNATURE.equals(sAcceptanceMethod) ? SmtaGenerator.sArticle10option1File : SmtaGenerator.sArticle10option2File))));
				 acceptanceMethodsWithSampleURLs.put(sAcceptanceMethod, url);
			}
			catch (Exception e)
			{
				throw new AccessDeniedException(e.getMessage());
			}
		return acceptanceMethodsWithSampleURLs;
	}
	
	@Override
	public LinkedHashMap<String, String> getSmtaPaymentMethods() throws AccessDeniedException
	{
		LinkedHashMap<String, String> paymentMethodsWithSampleURLs = new LinkedHashMap<String, String>();
		for (String sPaymentMethod : paymentMethods)
			try
			{
				 String url = tryAndConvertPathToURL(new File(SmtaGenerator.TEMPLATE_DOC_LOCATION + (PAYMENT_METHOD_6_7.equals(sPaymentMethod) ? SmtaGenerator.sAnnex2 : SmtaGenerator.sAnnexes3and4)));
				 paymentMethodsWithSampleURLs.put(sPaymentMethod, url);
			}
			catch (Exception e)
			{
				throw new AccessDeniedException(e.getMessage());
			}
		return paymentMethodsWithSampleURLs;
	}
	
	private String tryAndConvertPathToURL(File smtaFile) throws IOException
	{
		MessageContext msgContext = new WebServiceContextImpl().getMessageContext();
		if (msgContext != null)
		{	// try to return a URL rather than a local path (only possible if called as a WS and if smta.output.folder is set to be a subfolder of the webapp directory)
			HttpServletRequest request = (HttpServletRequest) msgContext.get(AbstractHTTPDestination.HTTP_REQUEST);
			String sWebAppPath = request.getRealPath(request.getServletPath());
			File webAppRoot = new File(sWebAppPath.substring(0, sWebAppPath.lastIndexOf(File.separator)));

			if (smtaFile.getCanonicalPath().startsWith(webAppRoot.getCanonicalPath()))
			{
				String requestURL = request.getRequestURL().toString();
				String smtaFileUrl = requestURL.substring(0, requestURL.indexOf("/webservices/")) + smtaFile.getCanonicalPath().substring(webAppRoot.getCanonicalPath().length());
				if (!"/".equals(File.separator))
					smtaFileUrl = smtaFileUrl.replaceAll("\\".equals(File.separator) ? "\\\\" : File.separator, "/");
				
				return smtaFileUrl;
			}
		}

		return smtaFile.getPath();
	}
	
	@Override
	@Transactional(propagation=Propagation.REQUIRED, readOnly=true)
	public List<String> getSMTA(Integer orderId) throws AccessDeniedException
	{	
		String sPathToSmtaFile = SmtaGenerator.OUTPUT_DOC_LOCATION + orderId + "_";
		ArrayList<String> result = new ArrayList<String>();
		
		Order order = orderDao.read(orderId);
		if (order == null)
			throw new AccessDeniedException("No order found for id " + orderId);
		
		try
		{
			File smtaFile = new File(sPathToSmtaFile + SmtaGenerator.SMTA_OUTPUT_FILENAME);
			if (!smtaFile.exists() || smtaFile.length() == 0)
			{
				LOG.debug("about to generate SMTA");
				SmtaGenerator.generateSMTA(order);
			}
	
			File addendumFile = new File(sPathToSmtaFile + SmtaGenerator.ADDENDUM_OUTPUT_FILENAME);
			boolean fWithAddendum = ((addendumFile.exists() && addendumFile.length() > 0) || SmtaGenerator.generateAddendum(order) != null);		
			
			MessageContext msgContext = new WebServiceContextImpl().getMessageContext();
			if (msgContext != null)
			{	// try to return a URL rather than a local path (only possible if called as a WS and if smta.output.folder is set to be a subfolder of the webapp directory)
				HttpServletRequest request = (HttpServletRequest) msgContext.get(AbstractHTTPDestination.HTTP_REQUEST);
				String sWebAppPath = request.getRealPath(request.getServletPath());
				File webAppRoot = new File(sWebAppPath.substring(0, sWebAppPath.lastIndexOf(File.separator)));
	
				if (smtaFile.getCanonicalPath().startsWith(webAppRoot.getCanonicalPath()))
				{
					String requestURL = request.getRequestURL().toString();
					String smtaFileUrl = requestURL.substring(0, requestURL.indexOf("/webservices/")) + smtaFile.getCanonicalPath().substring(webAppRoot.getCanonicalPath().length());
					if (!"/".equals(File.separator))
						smtaFileUrl = smtaFileUrl.replaceAll("\\".equals(File.separator) ? "\\\\" : File.separator, "/");
					result.add(smtaFileUrl);
					if (fWithAddendum)
						result.add(smtaFileUrl.replace(SmtaGenerator.SMTA_OUTPUT_FILENAME, SmtaGenerator.ADDENDUM_OUTPUT_FILENAME));
				}
			}
			else
			{
				result.add(smtaFile.getPath());
				result.add(addendumFile.getPath());
			}
		}
		catch (Exception e)
		{
			throw new AccessDeniedException(e.getMessage());
		}
		
		return result;
	}
	

	@Override
	@Transactional(propagation=Propagation.REQUIRED, readOnly=true)
	public List<String> getDistinctContactTitleList() {
		return contactDao.listDistinctTitles();
	}
	
	@Override
	@Transactional(propagation=Propagation.REQUIRED, readOnly=true)
	public List<DTO> getOrdersByContact(Integer contactId)
	{
		ArrayList<DTO> result = new ArrayList<DTO>();
		Contact ctc = contactDao.read(contactId);
		if (ctc == null)
			return null;
		
		for (Order order : ctc.getOrders())
			result.add(domainToDTO(order, Arrays.asList(new String[] {"orderStatus", "centreType", "contact"})));

		return result;
	}
	
	@Override
	@Transactional(propagation=Propagation.REQUIRED, readOnly=true)
	public DTO getOrderById(Integer orderId)
	{
		return domainToDTO(orderDao.read(orderId), Arrays.asList(new String[] {"orderStatus", "centreType", "contact"}));
	}
	
	@Override
	@Transactional(propagation=Propagation.REQUIRED, readOnly=true)
	public List<DTO> getOrderDetails(Integer orderId)
	{
		ArrayList<DTO> result = new ArrayList<DTO>();
		Order order = orderDao.read(orderId);
			for (AssOrderAccession aoa : order.getAssOrderAccessions())
				result.add(domainToDTO(aoa, Arrays.asList(new String[] {"accession", "materialType"})));

		return result;
	}
	
	@Override
	@Transactional(propagation=Propagation.REQUIRED, readOnly=true)
	public List<DTO> getShipmentsByOrder(Integer orderId)
	{
		ArrayList<DTO> result = new ArrayList<DTO>();
		Order order = orderDao.read(orderId);
		if (order == null)
			return null;
		
		for (Shipment shipment : order.getShipments())
			result.add(domainToDTO(shipment, Arrays.asList(new String[] {"country"})));

		return result;
	}
	
	@Override
	@Transactional(propagation=Propagation.REQUIRED, readOnly=true)
	public List<DTO> getShipmentDetails(Integer shipmentId)
	{
		ArrayList<DTO> result = new ArrayList<DTO>();
		Shipment shipment = shipmentDao.read(shipmentId);
			for (AssShipmentAccession asa : shipment.getAssShipmentAccessions())
				result.add(domainToDTO(asa, Arrays.asList(new String[] {"accession"})));

		return result;
	}

	@Override
	public List<String> getPredefinedPurposeList()
	{
		return Arrays.asList(TextConstants.get("order.purposeList").split(";"));
	}
	

	@Override
	@Transactional(propagation=Propagation.REQUIRED, readOnly=true)
	public Boolean getAnnex1StatusForAccession(String itcCode) throws AccessDeniedException
	{
		Accession acc = accessionDao.getByUniqueAttribute("itcCode", itcCode);
		if (acc == null)
			throw new AccessDeniedException("No accession exists with ITC code " + itcCode);
		
		return acc.getImproved();
	}
	
	@Transactional(propagation=Propagation.SUPPORTS)
	static private DTO domainToDTO(Serializable domainObject, List<String> childRecordsToInclude)
    {
    	if (domainObject == null)
    		return null;
    	
    	DTO dto = new DTO();
    	Class<? extends Serializable> domainClass = domainObject.getClass();
    	int nPositionToStripFrom = domainClass.getName().indexOf("$$EnhancerByCGLIB");
    	if (nPositionToStripFrom == -1)
    		nPositionToStripFrom = domainClass.getName().indexOf("_$$_javassist_");
    	if (nPositionToStripFrom > 0)
			try 
    		{
				domainClass = (Class<? extends Serializable>) Class.forName(domainClass.getName().substring(0, nPositionToStripFrom));
			}
    		catch (ClassNotFoundException e1)
			{
				LOG.warn("Unable to find class domain class", e1);
			}
    		
    	for (Field field : domainClass.getDeclaredFields())
    	{
    		String sFieldName = field.getName();
			List<String> forbiddenPropertiesForThisClass = BaseDao.getForbiddenPropertiesForClass(domainClass);
			if (forbiddenPropertiesForThisClass != null && forbiddenPropertiesForThisClass.contains(sFieldName))
				continue; // forbidden field

    		//if (!sFieldName.contains("$"))
	    		try
	    		{
					Method getter = domainClass.getDeclaredMethod("get" + sFieldName.substring(0, 1).toUpperCase() + sFieldName.substring(1));
					Serializable obj = (Serializable)getter.invoke(domainObject);
					if (childRecordsToInclude.contains(sFieldName) && obj != null && obj.getClass().getPackage().equals(domainObject.getClass().getPackage()))
					{	// it's a domain object which we got via a FK association
						List<String> childSubRecordsToInclude = new ArrayList<String>();
						for (String childRecordToInclude : childRecordsToInclude)
							if (childRecordToInclude.startsWith(sFieldName + "."))
								childSubRecordsToInclude.add(childRecordToInclude.substring(1 + sFieldName.length()));
								
						DTO subDTO = domainToDTO(obj, childSubRecordsToInclude);
						for (Class type : subDTO.getSupportedFieldTypes())
							for (String sSubRecordFieldName : subDTO.getFieldNames(type))
								dto.set(sFieldName + "." + sSubRecordFieldName, subDTO.get(sSubRecordFieldName));
					}
					else
						dto.set(sFieldName, obj);
				}
	    		catch (Exception e)
	    		{
	    			if (e instanceof java.io.NotSerializableException)
	    				LOG.trace("Unable to add property " + sFieldName + " to DTO for class " + domainClass.getName() + " (NotSerializableException)");
	    			else
	    				LOG.log(e instanceof NoSuchMethodException ? Priority.DEBUG : Priority.INFO, "Unable to add property " + sFieldName + " to DTO for class " + domainClass.getName(), e);
				}
    	}
    	return dto;
    }
	
	@Transactional(propagation=Propagation.SUPPORTS)
	static private DTO domainToDTO(Serializable domainObject)
    {
		return domainToDTO(domainObject, new ArrayList<String>());
    }
	
	@SuppressWarnings("unchecked")
	private void domainFromDTO(Serializable domainObject, DTO contents) throws Exception
    {	
		domainFromDTO(domainObject, contents, false);
    }
	
	@SuppressWarnings("unchecked")
	private void domainFromDTO(Serializable domainObject, DTO contents, boolean fDontAutoFillLastUpdated) throws Exception
    {	
    	Class<? extends Serializable> domainClass = domainObject.getClass();
    	boolean fLastUpdatedSpecifiedInDTO = false;
		for (Class type : contents.getSupportedFieldTypes())
			for (String sFieldName : contents.getFieldNames(type))
	    	{				
	    		Serializable value = contents.get(sFieldName);
	    		if (value == null || "id".equals(sFieldName))	// we don't want to deal with IDs here
	    			continue;
	    		
	    		try
	    		{
					Method setter = getFirstDeclaredMethodByName(domainClass, "set" + sFieldName.substring(0, 1).toUpperCase() + sFieldName.substring(1));
					if (!setter.getParameterTypes()[0].isAssignableFrom(value.getClass()) && setter.getParameterTypes()[0].getPackage().equals(domainClass.getPackage()))
					{	// it's a FK
						@SuppressWarnings("rawtypes")
						BaseDao associatedClassDao = getDaoByDomainClass(setter.getParameterTypes()[0]);
						value = (Serializable) associatedClassDao.read(value);
					}
					setter.invoke(domainObject, value);
				}
	    		catch (Exception e)
	    		{
	   				LOG.error("Unable to set property " + sFieldName + " for class " + domainClass.getName(), e);
				}
	    		
				if ("lastUpdated".equals(sFieldName))
					fLastUpdatedSpecifiedInDTO = true;
	    	}
		
		if (!fLastUpdatedSpecifiedInDTO)
		{
			try
			{
				Method setter = getFirstDeclaredMethodByName(domainClass, "setLastUpdated");
				setter.invoke(domainObject, new Date());
			}
			catch (Exception e)
			{
				if (!NoSuchMethodException.class.equals(e.getClass()))
					throw e;
			}
		}
    }
	
	private static Method getFirstDeclaredMethodByName(Class clazz, String sMethodName) throws NoSuchMethodException
	{
		ArrayList<Method> methods = new ArrayList<Method>();
		for (Method m : clazz.getDeclaredMethods())
			if (m.getName().equals(sMethodName))
			return m;

		throw new NoSuchMethodException(clazz.getName() + "." + sMethodName);
	}
	
	private BaseDao getDaoByDomainClass(Class domainClass) throws IllegalArgumentException, IllegalAccessException
	{
		for (Field field : MgbmsServiceImpl.class.getDeclaredFields())
			if (BaseDao.class.isAssignableFrom(field.getType()) && ((BaseDao) field.get(this)).getType().equals(domainClass))
				return (BaseDao) field.get(this);
		
		return null;
	}

}

